1. Annotation


In [1]:
def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

In [2]:
gcd(27, 36)


Out[2]:
9

In [3]:
gcd(2.7, 3.6)


Out[3]:
4.440892098500626e-16

In [4]:
gcd('12', '8')


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-08bfb65c0d4e> in <module>()
----> 1 gcd('12', '8')

<ipython-input-1-55e4926ac83e> in gcd(a, b)
      1 def gcd(a, b):
      2     while b:
----> 3         a, b = b, a % b
      4     return a

TypeError: not all arguments converted during string formatting

In [75]:
def gcd_2(a, b):
    assert isinstance(a, int), 'Expected int'
    assert isinstance(b, int), 'Expected int'
    
    while b:
        a, b = b, a % b
    return a

In [76]:
gcd_2(2.7, 3.6)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-76-d41432e1fea6> in <module>()
----> 1 gcd_2(2.7, 3.6)

<ipython-input-75-59f6549716fb> in gcd_2(a, b)
      1 def gcd_2(a, b):
----> 2     assert isinstance(a, int), 'Expected int'
      3     assert isinstance(b, int), 'Expected int'
      4 
      5     while b:

AssertionError: Expected int

In [77]:
gcd_2('12', '8')


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-77-cafaa0adbe43> in <module>()
----> 1 gcd_2('12', '8')

<ipython-input-75-59f6549716fb> in gcd_2(a, b)
      1 def gcd_2(a, b):
----> 2     assert isinstance(a, int), 'Expected int'
      3     assert isinstance(b, int), 'Expected int'
      4 
      5     while b:

AssertionError: Expected int

In [78]:
class Contract:
    @classmethod
    def check(cls, value):
        pass

class Integer(Contract):
    @classmethod
    def check(cls, value):
        assert isinstance(value, int), 'Expected int'

In [79]:
Integer.check(1)

In [80]:
Integer.check(1.5)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-80-46ff560006fa> in <module>()
----> 1 Integer.check(1.5)

<ipython-input-78-b0f848703994> in check(cls, value)
      7     @classmethod
      8     def check(cls, value):
----> 9         assert isinstance(value, int), 'Expected int'

AssertionError: Expected int

클래스로 만들어 놓은 단순히 반복되는 코드 import


In [5]:
from contract import Contract, Typed, Integer, Float, String, Positive, PositiveInteger

In [6]:
def gcd(a, b):
    Integer.check(a)
    Positive.check(a)
    Integer.check(b)
    Positive.check(b)

    while b:
        a, b = b, a % b
    return a

In [7]:
gcd(27, 36)


Out[7]:
9

In [8]:
gcd("wow", "such")


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-8-261f81669f5e> in <module>()
----> 1 gcd("wow", "such")

<ipython-input-6-176ce002629c> in gcd(a, b)
      1 def gcd(a, b):
----> 2     Integer.check(a)
      3     Positive.check(a)
      4     Integer.check(b)
      5     Positive.check(b)

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     22     @classmethod
     23     def check(cls, value):
---> 24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
     26         super().check(value)

AssertionError: Expected <class 'int'>

In [9]:
gcd(-1, 1)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-9-9079a3819812> in <module>()
----> 1 gcd(-1, 1)

<ipython-input-6-176ce002629c> in gcd(a, b)
      1 def gcd(a, b):
      2     Integer.check(a)
----> 3     Positive.check(a)
      4     Integer.check(b)
      5     Positive.check(b)

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     43     @classmethod
     44     def check(cls, value):
---> 45         assert value > 0, 'Must be > 0'
     46         super().check(value)
     47 

AssertionError: Must be > 0

In [10]:
def gcd(a, b):
    PositiveInteger.check(a)
    PositiveInteger.check(b)
    
    while b:
        a, b = b, a % b
    return a

In [11]:
gcd(-1, 1)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-11-9079a3819812> in <module>()
----> 1 gcd(-1, 1)

<ipython-input-10-4cda727ddeb0> in gcd(a, b)
      1 def gcd(a, b):
----> 2     PositiveInteger.check(a)
      3     PositiveInteger.check(b)
      4 
      5     while b:

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
---> 26         super().check(value)
     27         i = 1
     28 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     43     @classmethod
     44     def check(cls, value):
---> 45         assert value > 0, 'Must be > 0'
     46         super().check(value)
     47 

AssertionError: Must be > 0

In [12]:
gcd(27, 36)


Out[12]:
9

In [13]:
gcd.__annotations__


Out[13]:
{}

In [14]:
from inspect import signature
# inspect 모듈은 파이썬 오브젝트에 대한 정보를 가져옴
# inspect.signature함수는 callable객체의 시그니쳐 정보를 가져옴

In [15]:
signature(gcd)


Out[15]:
<Signature (a, b)>

In [16]:
signature(gcd).bind(1, 4)


Out[16]:
<BoundArguments (a=1, b=4)>

In [17]:
signature(gcd).bind(1, 4).arguments


Out[17]:
OrderedDict([('a', 1), ('b', 4)])

annotation과 signature를 비교한 검증


In [18]:
from contract import checked

In [19]:
@checked
def gcd(a: PositiveInteger, b: PositiveInteger):
    while b:
        a, b = b, a % b
    return a

In [20]:
gcd(11, 22)


Out[20]:
11

In [21]:
gcd(-11, 22)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-06de2db09d60> in <module>()
----> 1 gcd(-11, 22)

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in wrapper(*args, **kwargs)
     83         for name, val in bound.arguments.items():
     84             if name in ann:
---> 85                 ann[name].check(val)
     86         return func(*args, **kwargs)
     87     return wrapper

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
---> 26         super().check(value)
     27         i = 1
     28 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     43     @classmethod
     44     def check(cls, value):
---> 45         assert value > 0, 'Must be > 0'
     46         super().check(value)
     47 

AssertionError: Must be > 0

2. Class annotation


In [22]:
class Player:
    def __init__(self, name, x, y):
        self.name = name
        self.x = x
        self.y = y
    
    def left(self, dx):
        self.x -= dx
    
    def right(self, dx):
        self.x += dx

In [23]:
p = Player('아드', 10, 2)
p.x


Out[23]:
10

In [24]:
p.left(-5)

In [25]:
p.x


Out[25]:
15

In [26]:
p.x = '하핳 받아랏!!'
p.left(-5)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-26-f803ee5761ec> in <module>()
      1 p.x = '하핳 받아랏!!'
----> 2 p.left(-5)

<ipython-input-22-9222fb6353d9> in left(self, dx)
      6 
      7     def left(self, dx):
----> 8         self.x -= dx
      9 
     10     def right(self, dx):

TypeError: unsupported operand type(s) for -=: 'str' and 'int'

위와 같은 문제를 방지하기 위하여 getter/setter 즉, property를 사용


In [27]:
class Player:
    def __init__(self, name, x, y):
        self.name = name
        self.x = x
        self.y = y
    
    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        Integer.check(value)
        self._x = value

In [28]:
p = Player('아드', 0, 0)
p.x = 10

In [29]:
p.x = '멍츙'


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-29-ed3321c77d90> in <module>()
----> 1 p.x = '멍츙'

<ipython-input-27-38319cb4ae08> in x(self, value)
     11     @x.setter
     12     def x(self, value):
---> 13         Integer.check(value)
     14         self._x = value

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     22     @classmethod
     23     def check(cls, value):
---> 24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
     26         super().check(value)

AssertionError: Expected <class 'int'>

그러나 일일히 프로퍼티를 만드는 대신 클래스를 사용할 수 있음

  • Contract 클래스에 setset_name 메서드를 오버라이드하고 코드 수정

In [30]:
# 수정 전
class Contract:
    @classmethod
    def check(cls, value):
        pass

In [31]:
# 수정 후
class Contract:
    def __set__(self, instance, value):
        self.check(value)
        instance.__dict__[self.name] = value

    def __set_name__(self, owner, name):
        self.name = name

    @classmethod
    def check(cls, value):
        pass

Player 클래스는 프로퍼티 제거


In [32]:
from contract import NonEmpty, NonEmptyString

In [33]:
class Player:
    name = NonEmptyString()
    x = Integer()
    y = Integer()

    def __init__(self, name, x, y):
        self.name = name
        self.x = x
        self.y = y

In [34]:
p = Player('아드', 0, 0)

In [35]:
p.name = 10


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-35-dd31de3bd446> in <module>()
----> 1 p.name = 10

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in __set__(self, instance, value)
      8 
      9     def __set__(self, instance, value):
---> 10         self.check(value)
     11         instance.__dict__[self.name] = value
     12 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     22     @classmethod
     23     def check(cls, value):
---> 24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
     26         super().check(value)

AssertionError: Expected <class 'str'>

In [36]:
p.name = ''


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-36-1ae0620f7a78> in <module>()
----> 1 p.name = ''

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in __set__(self, instance, value)
      8 
      9     def __set__(self, instance, value):
---> 10         self.check(value)
     11         instance.__dict__[self.name] = value
     12 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
---> 26         super().check(value)
     27         i = 1
     28 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     55     @classmethod
     56     def check(cls, value):
---> 57         assert len(value) > 0, 'Must be nonempty'
     58         super().check(value)
     59 

AssertionError: Must be nonempty

In [37]:
class Player:
    name: NonEmptyString
    x: Integer
    y: Integer

In [38]:
Player.__annotations__


Out[38]:
{'name': contract.NonEmptyString, 'x': contract.Integer, 'y': contract.Integer}

In [39]:
class Base:
    @classmethod
    def __init_subclass__(cls):
        for name, val in cls.__annotations__.items():
            contract = val()
            contract.__set_name__(cls, name)
            setattr(cls, name, contract)
            
class Player(Base):
    name: NonEmptyString
    x: Integer
    y: Integer
    def __init__(self, name, x, y):
        self.name = name
        self.x = x
        self.y = y

In [40]:
p = Player('아드', 0, 0)
p.name = 12


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-40-11624e1633cc> in <module>()
      1 p = Player('아드', 0, 0)
----> 2 p.name = 12

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in __set__(self, instance, value)
      8 
      9     def __set__(self, instance, value):
---> 10         self.check(value)
     11         instance.__dict__[self.name] = value
     12 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     22     @classmethod
     23     def check(cls, value):
---> 24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
     26         super().check(value)

AssertionError: Expected <class 'str'>

In [41]:
class Base:
    @classmethod
    def __init_subclass__(cls):
        for name, val in cls.__annotations__.items():
            contract = val()
            contract.__set_name__(cls, name)
            setattr(cls, name, contract)
            
    def __init__(self, *args):
        ann = self.__annotations__
        assert len(ann) == len(args), f'Expected {len(ann)} arguments'
        for name, val in zip(ann, args):
            setattr(self, name, val)

    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self.__annotations__)
        return f'{type(self).__name__}({args})'
    
class Player(Base):
    name: NonEmptyString
    x: Integer
    y: Integer

    def left(self, dx):
        self.x -= dx

    def right(self, dx):
        self.x += dx

In [42]:
p = Player('아드', 0, 0)
p


Out[42]:
Player('아드', 0, 0)

In [43]:
p.x


Out[43]:
0

In [44]:
p.x='코드엑스'


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-44-6963de8e8e9d> in <module>()
----> 1 p.x='코드엑스'

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in __set__(self, instance, value)
      8 
      9     def __set__(self, instance, value):
---> 10         self.check(value)
     11         instance.__dict__[self.name] = value
     12 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     22     @classmethod
     23     def check(cls, value):
---> 24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
     26         super().check(value)

AssertionError: Expected <class 'int'>

In [46]:
def chcked(func):
    sig = signature(func)
    ann = func.__annotations__
    @wraps(func)
    def wrapper(*args, **kwargs):
        bound = sig.bind(*args, **kwargs)
        for name, val in bound.arguments.items():
            if name in ann:
                ann[name].check(val)
        return func(*args, **kwargs)
    return wrapper

class Base:
    @classmethod
    def __init_subclass__(cls):
        for name, val in cls.__dict__.items():
            if callable(val):
                setattr(cls, name, chcked(val))

        for name, val in cls.__annotations__.items():
            contract = val()
            contract.__set_name__(cls, name)
            setattr(cls, name, contract)

    def __init__(self, *args):
        ann = self.__annotations__
        assert len(ann) == len(args), f'Expected {len(ann)} arguments'
        for name, val in zip(ann, args):
            setattr(self, name, val)

    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self.__annotations__)
        return f'{type(self).__name__}({args})'


class Player(Base):
    name: NonEmptyString
    x: Integer
    y: Integer

    def left(self, dx: PositiveInteger):
        self.x -= dx

    def right(self, dx: PositiveInteger):
        self.x += dx

In [47]:
p = Player('아드', 0, 0)

In [48]:
p


Out[48]:
Player('아드', 0, 0)

In [49]:
p.left(0)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-49-a3ad5e2d2961> in <module>()
----> 1 p.left(0)

<ipython-input-46-4176ad3b5714> in wrapper(*args, **kwargs)
      7         for name, val in bound.arguments.items():
      8             if name in ann:
----> 9                 ann[name].check(val)
     10         return func(*args, **kwargs)
     11     return wrapper

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
---> 26         super().check(value)
     27         i = 1
     28 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     43     @classmethod
     44     def check(cls, value):
---> 45         assert value > 0, 'Must be > 0'
     46         super().check(value)
     47 

AssertionError: Must be > 0

3. 메타클래스


In [50]:
from collections import OrderedDict
dic = {}


class Meta(type):
    @classmethod
    def __prepare__(cls, *args):
        # __prepare__는 클래스의 어트리뷰트와 매서드들을 키와 벨류로 가지는 딕셔너리를 리턴
        global dic
        dic = OrderedDict()
        return dic

class Base(metaclass=Meta):
    a = 'wow'
    def __init__(self):
        self.name = '아드'
        self.x = 0
        self.y = 42

print(dic)


OrderedDict([('__module__', '__main__'), ('__qualname__', 'Base'), ('a', 'wow'), ('__init__', <function Base.__init__ at 0x7fe77c5e3d90>)])

In [51]:
class Meta(type):
    @classmethod
    def __prepare__(cls, *args):
        def see_me():
            return '내가 보이니..?'
        return {'see_me': see_me}

class Base(metaclass=Meta):
    a = see_me()

print(Base.a)


내가 보이니..?

In [52]:
class Meta(type):
    @classmethod
    def __prepare__(cls, *args):
        return {}

    def __new__(meta, name, bases, methods):
        print(f'name : "{name}", methods: {methods}')
        return super().__new__(meta, name, bases, methods)

class Base(metaclass=Meta):
    pass


name : "Base", methods: {'__module__': '__main__', '__qualname__': 'Base'}

In [53]:
class Meta(type):
    @classmethod
    def __prepare__(cls, *args):
        def see_me():
            return '내가 보이니..?'
        return {'see_me': see_me}

    def __new__(meta, name, bases, methods):
        return super().__new__(meta, name, bases, methods)


class Base(metaclass=Meta):
    pass

print(Base.see_me())


내가 보이니..?

클래스 외부에서 어트리뷰트로 see_me에 접근할 수 있는 문제를 해결하기 위하여 collections.ChainMap 사용


In [55]:
from collections import ChainMap

In [56]:
dic = None

class Meta(type):
    @classmethod
    def __prepare__(cls, *args):
        def see_me():
            return '내가 보이니..?'
        global dic
        dic = ChainMap({}, {'see_me': see_me})
        return dic

    def __new__(meta, name, bases, methods):
        methods = methods.maps[0]
        return super().__new__(meta, name, bases, methods)

class Base(metaclass=Meta):
    a = see_me()

print(Base.a)
print('see_me' in Base.__dict__.keys())


내가 보이니..?
False

chainmap 예시


In [57]:
c = ChainMap({}, {'x': 0, 'y': 0})
c['x']


Out[57]:
0

In [58]:
c['a'] = 42
c


Out[58]:
ChainMap({'a': 42}, {'x': 0, 'y': 0})

In [59]:
c.maps[0]


Out[59]:
{'a': 42}

In [60]:
c['y']


Out[60]:
0

In [68]:
class BaseMeta(type):
    @classmethod
    def __prepare__(cls, *args):
        return ChainMap({}, _contracts)

    def __new__(meta, name, bases, methods):
        methods = methods.maps[0]
        return super().__new__(meta, name, bases, methods)

In [72]:
def chcked(func):
    sig = signature(func)
    ann = func.__annotations__
    @wraps(func)
    def wrapper(*args, **kwargs):
        bound = sig.bind(*args, **kwargs)
        for name, val in bound.arguments.items():
            if name in ann:
                ann[name].check(val)
        return func(*args, **kwargs)
    return wrapper

class Base:
    @classmethod
    def __init_subclass__(cls):
        for name, val in cls.__dict__.items():
            if callable(val):
                setattr(cls, name, chcked(val))

        for name, val in cls.__annotations__.items():
            contract = val()
            contract.__set_name__(cls, name)
            setattr(cls, name, contract)

    def __init__(self, *args):
        ann = self.__annotations__
        assert len(ann) == len(args), f'Expected {len(ann)} arguments'
        for name, val in zip(ann, args):
            setattr(self, name, val)

    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self.__annotations__)
        return f'{type(self).__name__}({args})'

class Player(Base):
    name: NonEmptyString
    x: Integer
    y: Integer

    def left(self, dx: PositiveInteger):
        self.x -= dx

    def right(self, dx: PositiveInteger):
        self.x += dx

In [73]:
p = Player('아드', 0, 0)
p.left(-1)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-73-7ecbc8d6e4cd> in <module>()
      1 p = Player('아드', 0, 0)
----> 2 p.left(-1)

<ipython-input-72-c02a750b07c6> in wrapper(*args, **kwargs)
      7         for name, val in bound.arguments.items():
      8             if name in ann:
----> 9                 ann[name].check(val)
     10         return func(*args, **kwargs)
     11     return wrapper

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
---> 26         super().check(value)
     27         i = 1
     28 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     43     @classmethod
     44     def check(cls, value):
---> 45         assert value > 0, 'Must be > 0'
     46         super().check(value)
     47 

AssertionError: Must be > 0

In [74]:
from contract import Base, PositiveInteger

dx: PositiveInteger

class Player(Base):
    name: NonEmptyString
    x: Integer
    y: Integer

    def left(self, dx):
        self.x -= dx

    def right(self, dx):
        self.x += dx

p = Player('아드', 0, 0)
p.left(-1)


---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-74-42b635d8fb6a> in <module>()
     15 
     16 p = Player('아드', 0, 0)
---> 17 p.left(-1)

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in wrapper(*args, **kwargs)
     83         for name, val in bound.arguments.items():
     84             if name in ann:
---> 85                 ann[name].check(val)
     86         return func(*args, **kwargs)
     87     return wrapper

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     24         assert isinstance(value, cls.type), f'Expected {cls.type}'
     25         # f'a = {a}' 은 PEP 498의 F-String 이라는 문법
---> 26         super().check(value)
     27         i = 1
     28 

/media/seongcheolkim/E4260B2A260AFD74/workspace/issue/contract.py in check(cls, value)
     43     @classmethod
     44     def check(cls, value):
---> 45         assert value > 0, 'Must be > 0'
     46         super().check(value)
     47 

AssertionError: Must be > 0